﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Media3D;
using Petzold.Media3D;
using System.Windows.Media;
using System.IO;
using System.Xml.Linq;

namespace GeometryViz3D.Model
{
    public class G3DModel
    {
        const double POINT_LABEL_SIZE = 0.3;
        const double LINE_THICKNESS = 2;
        const string UNTITLED = "Untitled";

        List<G3DPoint> m_points = new List<G3DPoint>();
        List<G3DLine> m_lines = new List<G3DLine>();
        List<G3DPlane> m_planes = new List<G3DPlane>();

        Dictionary<string, G3DPoint> m_pointsByName = new Dictionary<string, G3DPoint>();
        Dictionary<string, G3DLine> m_linesByName = new Dictionary<string, G3DLine>();
        Dictionary<string, G3DPlane> m_planesByName = new Dictionary<string, G3DPlane>();

        HashSet<string> m_ids = new HashSet<string>();

        bool m_isDirty = false;

        public G3DModel()
        {
            Extent = 0;
            FileName = UNTITLED;
        }

        public string FileName
        {
            get;
            set;
        }

        public bool IsUntitled
        {
            get { return FileName == UNTITLED; }
        }

        public List<G3DPoint> Points
        {
            get { return m_points; }
        }

        public List<G3DLine> Lines
        {
            get { return m_lines; }
        }

        public List<G3DPlane> Planes
        {
            get { return m_planes; }
        }

        public double Extent
        {
            get;
            private set;
        }

        public bool IsEmpty
        {
            get { return m_points.Count == 0; }
        }

        public bool IsDirty
        {
            get { return m_isDirty; }
            set { m_isDirty = value; }
        }

        public G3DModel Clone()
        {
            G3DModel model = new G3DModel();

            m_points.ForEach(p => model.AddPoint(p));
            m_lines.ForEach(l => model.AddLine(l));

            return model;
        }

        public void Clear()
        {
            m_points.Clear();
            m_lines.Clear();

            m_pointsByName.Clear();
            m_linesByName.Clear();
        }

        public void AddPoint(string label, Point3D position)
        {
            G3DPoint p = new G3DPoint() { Position = position, Label = label };
            AddPoint(p);
        }

        private void AddPoint(G3DPoint p)
        {
            if (m_ids.Contains(p.Label))
            {
                throw new G3DDuplicateIDException(p.Label);
            }

            m_ids.Add(p.Label);

            m_points.Add(p);
            m_pointsByName[p.Label] = p;

            Extent = Math.Max(Extent, Math.Abs(p.Position.X));
            Extent = Math.Max(Extent, Math.Abs(p.Position.Y));
            Extent = Math.Max(Extent, Math.Abs(p.Position.Z));

            m_isDirty = true;
        }

        public void RemovePoint(string label)
        {
            if (m_pointsByName.ContainsKey(label))
            {
                m_points.Remove(m_pointsByName[label]);
                m_pointsByName.Remove(label);

                m_isDirty = true;
            }
        }

        public void AddLine(string startPoint, string endPoint, string color)
        {
            Color c = G3DColors.GetColor(color);

            if (!m_pointsByName.ContainsKey(startPoint))
            {
                throw new G3DPointNotExistException(startPoint);
            }

            if (!m_pointsByName.ContainsKey(endPoint))
            {
                throw new G3DPointNotExistException(endPoint);
            }

            AddLine(m_pointsByName[startPoint], m_pointsByName[endPoint], c);
        }

        public void AddLine(G3DPoint startPoint, G3DPoint endPoint, Color color)
        {
            G3DLine l = new G3DLine()
            {
                Label = string.Format("{0}{1}", startPoint.Label, endPoint.Label),
                StartPoint = startPoint,
                EndPoint = endPoint,
                Color = color
            };

            AddLine(l);
        }

        private void AddLine(G3DLine l)
        {
            if (m_ids.Contains(l.Label))
            {
                throw new G3DDuplicateIDException(l.Label);
            }

            m_ids.Add(l.Label);

            m_lines.Add(l);
            m_linesByName[l.Label] = l;

            FindPlanes();

            m_isDirty = true;
        }

        public void RemoveLine(string label)
        {
            if (m_linesByName.ContainsKey(label))
            {
                m_lines.Remove(m_linesByName[label]);
                m_linesByName.Remove(label);

                m_isDirty = true;
            }
        }

        public void SaveAs(string fileName)
        {
            FileName = fileName;

            Save();
        }

        public void Save()
        {
            XDocument doc = new XDocument();
            XElement root = new XElement("G3DModel");
            doc.Add(root);

            XElement points = new XElement("Points");
            root.Add(points);

            Points.ForEach(p =>
                {
                    XElement point = new XElement("Point");
                    point.Add(new XAttribute("Label", p.Label));
                    point.Add(new XAttribute("X", p.Position.X));
                    point.Add(new XAttribute("Y", p.Position.Y));
                    point.Add(new XAttribute("Z", p.Position.Z));

                    points.Add(point);
                });

            XElement lines = new XElement("Lines");
            root.Add(lines);

            Lines.ForEach(l =>
                {
                    XElement line = new XElement("Line");
                    line.Add(new XAttribute("StartPoint", l.StartPoint.Label));
                    line.Add(new XAttribute("EndPoint", l.EndPoint.Label));
                    line.Add(new XAttribute("Color", G3DColors.GetColorName(l.Color.ToString().ToUpper())));
                    
                    lines.Add(line);
                });

            XElement planes = new XElement("Planes");
            root.Add(planes);

            Planes.ForEach(pl =>
                {
                    XElement plane = new XElement("Plane");

                    planes.Add(plane);
                });

            doc.Save(FileName);

            m_isDirty = false;
        }

        public static G3DModel Load(string fileName)
        {
            XDocument doc = XDocument.Load(fileName);
            XElement root = doc.Element("G3DModel");

            G3DModel model = new G3DModel();
            model.FileName = fileName;

            foreach (var e in root.Element("Points").Elements("Point"))
            {
                double x = 0, y = 0, z = 0;
                Double.TryParse((string)e.Attribute("X"), out x);
                Double.TryParse((string)e.Attribute("Y"), out y);
                Double.TryParse((string)e.Attribute("Z"), out z);

                G3DPoint pt = new G3DPoint()
                {
                    Label = (string)e.Attribute("Label"),
                    Position = new Point3D(x, y, z)
                };

                model.AddPoint(pt);
            }

            foreach (var e in root.Element("Lines").Elements("Line"))
            {
                model.AddLine
                    (
                    (string)e.Attribute("StartPoint"),
                    (string)e.Attribute("EndPoint"),
                    (string)e.Attribute("Color")
                    );
            }

            foreach (var e in root.Element("Planes").Elements("Plane"))
            {
                //model.AddPlane();
            }

            return model;
        }

        /// <summary>
        /// Creates and returns a ModelVisual3D that represents the model.
        /// </summary>
        /// <param name="ratio">The ratio to be used to display the labels.
        /// </param>
        /// <returns>The ModelVisual3D that represents the model</returns>
        public ModelVisual3D CreateVisual3D(double ratio)
        {
            ModelVisual3D visual = new ModelVisual3D();

            AddLines(visual);
            AddPoints(visual, ratio);

            return visual;
        }

        /// <summary>
        /// Add all lines of the model to the ModelVisual3D object.
        /// </summary>
        /// <param name="visual">The ModelVisual3D to which the lines are added.</param>
        private void AddLines(ModelVisual3D visual)
        {
            foreach (var line in m_linesByName.Values)
            {
                if (line.StartPoint != null && line.EndPoint != null)
                {
                    var wl = new WireLine()
                    {
                        Point1 = line.StartPoint.Position,
                        Point2 = line.EndPoint.Position,
                        Color = line.Color,
                        Thickness = LINE_THICKNESS
                    };

                    visual.Children.Add(wl);
                }
            }
        }

        /// <summary>
        /// Add points and labels to the ModelVisual3D object.
        /// </summary>
        /// <param name="visual">The ModelVisual3D to which the points 
        /// and labels are added</param>
        /// <param name="ratio">The ratio to scale the labels</param>
        private void AddPoints(ModelVisual3D visual, double ratio)
        {
            foreach (var p in m_pointsByName.Values)
            {
                if (!IsPointUsedInLines(p))
                {
                    var point = 1.0/96.0;

                    var wl = new WireLine()
                    {
                        Point1 = p.Position,
                        Point2 = p.Position + new Vector3D(point, point, point),
                        Thickness = 2
                    };

                    visual.Children.Add(wl);
                }

                if (!string.IsNullOrEmpty(p.Label))
                {
                    var wt = new WireText()
                    {
                        Origin = p.Position,
                        Text = p.Label,
                        FontSize = POINT_LABEL_SIZE * ratio,
                        Thickness = 2
                    };

                    visual.Children.Add(wt);
                }
            }
        }

        /// <summary>
        /// Check if the given point is used in any line.
        /// </summary>
        /// <param name="p">The point to be checked</param>
        /// <returns>Returns true if the point is used in a line.</returns>
        private bool IsPointUsedInLines(G3DPoint p)
        {
            foreach (var l in m_lines)
            {
                if (l.StartPoint.Label == p.Label || l.EndPoint.Label == p.Label)
                {
                    return true;
                }
            }

            return false;
        }

        private IEnumerable<G3DElement> ToEnumerable<T>(List<T> collection)
            where T : G3DElement
        {
            foreach (var e in collection)
            {
                yield return e;
            }
        }

#region Find Planes from Lines

        class LinesOnPlane
        {
            List<G3DPoint> m_points = new List<G3DPoint>();
            List<G3DLine> m_lines = new List<G3DLine>();

            internal LinesOnPlane(G3DLine l1, G3DLine l2)
            {
                m_lines.Add(l1);
                m_lines.Add(l2);

                m_points.Add(l1.StartPoint);
                m_points.Add(l1.EndPoint);
                if (l2.StartPoint.Equals(l1.EndPoint))
                {
                    m_points.Add(l2.EndPoint);
                }
                else
                {
                    m_points.Add(l2.StartPoint);
                }
            }

            internal bool TryAddLine(G3DLine line)
            {
                if (IsOnPlane(line.StartPoint) && IsOnPlane(line.EndPoint))
                {
                    m_lines.Add(line);
                    return true;
                }
                else
                {
                    return false;
                }
            }

            internal bool IsClosed
            {
                get
                {
                    bool b = false;

                    return b;
                }
            }

            internal G3DPlane CreatePlane()
            {
                G3DPlane p = new G3DPlane();

                return p;
            }

            internal bool IsOnPlane(G3DPoint point)
            {
                foreach (var p in m_points)
                {
                    if (p.Equals(point))
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        private void FindPlanes()
        {
            List<LinesOnPlane> areas = FindClosedAreas();

            foreach (LinesOnPlane area in areas)
            {
                if (area.IsClosed)
                {
                    G3DPlane p = area.CreatePlane();

                    if (!ExistPlane(p))
                    {
                        m_planes.Add(p);
                    }
                }
            }
        }

        private List<LinesOnPlane> FindClosedAreas()
        {
            List<LinesOnPlane> planes = new List<LinesOnPlane>();

            foreach (var line in m_lines)
            {
                FormPlanes(planes, line);
            }

            return planes;
        }

        private void FormPlanes(List<LinesOnPlane> planes, G3DLine first)
        {
            foreach (var next in m_lines)
            {
                if (!next.Equals(first))
                {
                    if (next.StartPoint.Equals(first.EndPoint) || next.EndPoint.Equals(first.EndPoint))
                    {
                        LinesOnPlane plane = new LinesOnPlane(first, next);
                        if (TryFormPlane(plane, next))
                        {
                            planes.Add(plane);
                        }
                    }
                }
            }
        }

        private bool TryFormPlane(LinesOnPlane plane, G3DLine curr)
        {
            foreach (var next in m_lines)
            {
                if (!next.Equals(curr))
                {
                    if (next.StartPoint.Equals(curr.EndPoint) || next.EndPoint.Equals(curr.EndPoint))
                    {
                        if (plane.TryAddLine(next))
                        {
                            if (plane.IsClosed)
                            {
                                return true;
                            }
                            else
                            {
                                return TryFormPlane(plane, next);
                            }
                        }
                        else
                        {
                            return false;
                        }
                    }
                }
            }

            return false;
        }

        private bool ExistPlane(G3DPlane plane)
        {
            var planes = from p in m_planes
                         where p.Equals(plane)
                         select p;

            return planes.Any();
        }

        #endregion
    }
}
